/**
 * Copyright (c) 2023 OceanBase
 * OceanBase CE is licensed under Mulan PubL v2.
 * You can use this software according to the terms and conditions of the Mulan PubL v2.
 * You may obtain a copy of Mulan PubL v2 at:
 *          http://license.coscl.org.cn/MulanPubL-2.0
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PubL v2 for more details.
 */

#define USING_LOG_PREFIX SHARE

#include "ob_kerberos.h"
#include "share/ob_errno.h"

namespace oceanbase {
namespace share {

ObKerberos::~ObKerberos()
{
  if (OB_NOT_NULL(k5_data_.ctx)) {
    if (OB_NOT_NULL(default_cache_)) {
      krb5_cc_close(k5_data_.ctx, default_cache_);
    }
    krb5_free_principal(k5_data_.ctx, default_princ_);

    if (OB_NOT_NULL(options_)) {
      krb5_get_init_creds_opt_free(k5_data_.ctx, options_);
    }
    if (OB_LIKELY(creds_.client == k5_data_.princ)) {
      creds_.client = nullptr;
    }
    krb5_free_cred_contents(k5_data_.ctx, &creds_);
    if (OB_NOT_NULL(keytab_)) {
      krb5_kt_close(k5_data_.ctx, keytab_);
    }
  }
  reset_();
  // Free context itself
  k5_data_.cleanup();
}

int ObKerberos::init(common::ObString &keytab, common::ObString &principal,
                     common::ObString &conf_file,
                     common::ObString &cache_name)
{
  int ret = OB_SUCCESS;
  if (OB_UNLIKELY(!check_file_exists(keytab.ptr()))) {
    ret = OB_KERBEROS_ERROR;
    LOG_WARN("keytab file does not exist", K(ret), K(keytab));
  } else if (OB_LIKELY(!conf_file.empty())) {
    if (OB_UNLIKELY(!check_file_exists(conf_file.ptr()))) {
      ret = OB_KERBEROS_ERROR;
      LOG_WARN("invalid krb5 conf file path", K(ret));
    } else {
      const char* config_path = conf_file.ptr();
      // Note: this step is a TRICKY operation to workround if krb5.conf is not
      // in default path /etc/krb5.conf !!!
      if (0 != setenv("KRB5_CONFIG", config_path, 1)) {
        ret = OB_KERBEROS_ERROR;
        LOG_WARN("failed to set env variables for krb5 context", K(ret));
      } else {
        LOG_INFO("setup env for krb5.conf path", K(ret), K(config_path));
      }
    }
  } else {
    LOG_TRACE("config path is using default: /etc/krb5.conf", K(ret));
  }
  // 0 means success, other is err code.
  krb5_error_code err_code;
  if (OB_FAIL(ret)) {
  } else {
    err_code = krb5_init_context(&k5_data_.ctx);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      LOG_WARN("error while initializing Kerberos 5 library", K(ret), K(err_code));
    }
  }

  const char *deftype = nullptr;
  if (OB_FAIL(ret)) {
  } else if (OB_NOT_NULL(cache_name) && OB_LIKELY(0 != cache_name.length())) {
    err_code =
        krb5_cc_resolve(k5_data_.ctx, cache_name.ptr(), &k5_data_.out_cc);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      LOG_WARN("Error in resolving cache", K(ret), K(err_code), K(cache_name));
    }
  } else {
    // Resolve the default cache and get its type and default principal (if it
    // is initialized).
    err_code = krb5_cc_default(k5_data_.ctx, &default_cache_);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("error while getting default cache", K(ret), K(err_code), K(err_msg));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    } else {
      LOG_INFO("resolve principal", K(ret));
      deftype = krb5_cc_get_type(k5_data_.ctx, default_cache_);
      if (OB_UNLIKELY(0 != krb5_cc_get_principal(k5_data_.ctx, default_cache_,
                                                 &default_princ_))) {
        default_princ_ = nullptr;
      }
    }
  }

  if (OB_FAIL(ret)) {
  } else {
    // Use the specified principal name.
    err_code = krb5_parse_name_flags(k5_data_.ctx, principal.ptr(), 0,
                                     &k5_data_.princ);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("error when parsing principal name", K(ret), K(err_code),
               K(err_msg), K(principal));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    }
  }

  // Cache related commands
  if (OB_FAIL(ret)) {
  } else if (OB_ISNULL(k5_data_.out_cc) &&
             krb5_cc_support_switch(k5_data_.ctx, deftype)) {
    // Use an existing cache for the client principal if we can.
    err_code =
        krb5_cc_cache_match(k5_data_.ctx, k5_data_.princ, &k5_data_.out_cc);
    if (OB_UNLIKELY(0 != err_code && KRB5_CC_NOTFOUND != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("error while searching for cache", K(ret), K(err_code),
               K(err_msg), K(principal));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    } else if (OB_LIKELY(0 == err_code)) {
      const char *cc_name = krb5_cc_get_name(k5_data_.ctx, k5_data_.out_cc);
      LOG_WARN("using default cache", K(ret), K(cc_name));
      k5_data_.switch_to_cache = 1;
    } else if (OB_NOT_NULL(default_princ_)) {
      // Create a new cache to avoid overwriting the initialized default cache.
      err_code =
          krb5_cc_new_unique(k5_data_.ctx, deftype, nullptr, &k5_data_.out_cc);
      if (OB_UNLIKELY(0 != err_code)) {
        ret = OB_KERBEROS_ERROR;
        const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
        LOG_WARN("error while generating new cache", K(ret), K(err_code),
                 K(err_msg));
        // Note: free error message which generated by krb5_get_error_message.
        krb5_free_error_message(k5_data_.ctx, err_msg);
      } else {
        const char *cc_name = krb5_cc_get_name(k5_data_.ctx, k5_data_.out_cc);
        LOG_WARN("using default cache", K(ret), K(cc_name));
        k5_data_.switch_to_cache = 1;
      }
    }
  }

  if (OB_FAIL(ret)) {
  } else if (OB_ISNULL(k5_data_.out_cc)) {
    // Use the default cache if we haven't picked one yet.
    k5_data_.out_cc = default_cache_;
    default_cache_ = nullptr;
    const char *cc_name = krb5_cc_get_name(k5_data_.ctx, k5_data_.out_cc);
    LOG_INFO("using default cache", K(ret), K(cc_name));
  }

  if (OB_FAIL(ret)) {
  } else {
    err_code = krb5_unparse_name(k5_data_.ctx, k5_data_.princ, &k5_data_.name);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("error when unparsing name", K(ret), K(err_code), K(err_msg));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    } else {
      const char *name = k5_data_.name;
      LOG_TRACE("using principal", K(ret), K(name));
    }
  }

  if (OB_FAIL(ret)) {
  } else {
    // Allocate a new initial credential options structure.
    err_code = krb5_get_init_creds_opt_alloc(k5_data_.ctx, &options_);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);

      LOG_WARN("error in options allocation", K(ret), K(err_code), K(err_msg));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    }
  }

  if (OB_FAIL(ret)) {
  } else {
    // Resolve keytab
    err_code = krb5_kt_resolve(k5_data_.ctx, keytab.ptr(), &keytab_);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("error in resolving keytab", K(ret), K(err_code), K(err_msg), K(keytab));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    } else {
      LOG_TRACE("using keytab", K(ret), K(keytab));
    }
  }

  if (OB_FAIL(ret)) {
  } else {
    // Set an output credential cache in initial credential options.
    err_code = krb5_get_init_creds_opt_set_out_ccache(k5_data_.ctx, options_,
                                                      k5_data_.out_cc);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("error in setting output credential cache", K(ret), K(err_code), K(err_msg));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    }
  }

  // Action: init or renew
  LOG_INFO("trying to renew credentials", K(ret));
  if (OB_FAIL(ret)) {
  } else {
    memset(&creds_, 0, sizeof(creds_));
    // Get renewed credential from KDC using an existing credential from output
    // cache.
    err_code = krb5_get_renewed_creds(k5_data_.ctx, &creds_, k5_data_.princ,
                                      k5_data_.out_cc, nullptr);
    if (OB_UNLIKELY(0 != err_code)) {
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("trying to get initial credentials again", K(ret), K(err_msg));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
      // Request KDC for an initial credentials using keytab.
      err_code = krb5_get_init_creds_keytab(
          k5_data_.ctx, &creds_, k5_data_.princ, keytab_, 0, nullptr, options_);
      if (OB_UNLIKELY(0 != err_code)) {
        ret = OB_KERBEROS_ERROR;
        const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
        LOG_WARN("error in getting initial credentials", K(ret), K(err_code), K(err_msg));
        // Note: free error message which generated by krb5_get_error_message.
        krb5_free_error_message(k5_data_.ctx, err_msg);
      } else {
        LOG_TRACE("got initial credentials", K(ret));
      }
    } else {
      LOG_TRACE("successful renewal step", K(ret));
      // Initialize a credential cache. Destroy any existing contents of cache
      // and initialize it for the default principal.
      err_code =
          krb5_cc_initialize(k5_data_.ctx, k5_data_.out_cc, k5_data_.princ);
      if (OB_UNLIKELY(0 != err_code)) {
        ret = OB_KERBEROS_ERROR;
        const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
        LOG_WARN("error when initializing cache", K(ret), K(err_code), K(err_msg));
        // Note: free error message which generated by krb5_get_error_message.
        krb5_free_error_message(k5_data_.ctx, err_msg);
      } else {
        LOG_TRACE("initialized cache step", K(ret));
        // Store credentials in a credential cache.
        err_code = krb5_cc_store_cred(k5_data_.ctx, k5_data_.out_cc, &creds_);
        if (OB_UNLIKELY(0 != err_code)) {
          ret = OB_KERBEROS_ERROR;
          LOG_WARN("Error while storing credentials", K(ret), K(err_code));
        } else {
          LOG_INFO("stored credentials success", K(ret));
        }
      }
    }
  }

  if (OB_FAIL(ret)) {
  } else if (OB_LIKELY(k5_data_.switch_to_cache)) {
    // Make a credential cache the primary cache for its collection.
    err_code = krb5_cc_switch(k5_data_.ctx, k5_data_.out_cc);
    if (OB_UNLIKELY(0 != err_code)) {
      ret = OB_KERBEROS_ERROR;
      const char *err_msg = krb5_get_error_message(k5_data_.ctx, err_code);
      LOG_WARN("error while switching to new cache", K(ret), K(err_code), K(err_msg));
      // Note: free error message which generated by krb5_get_error_message.
      krb5_free_error_message(k5_data_.ctx, err_msg);
    }
  }

  LOG_TRACE("authenticated to kerberos v5", K(ret));
  return ret;
}

void ObKerberos::reset_() {
  default_cache_ = nullptr;
  options_ = nullptr;
  keytab_ = nullptr;
  default_princ_ = nullptr;
}

bool ObKerberos::check_file_exists(const char *path)
{
  struct stat buf;
  return 0 == ::stat(path, &buf) && S_ISREG(buf.st_mode);
}

} // namespace share
} // namespace oceanbase